package nl.Viewer.ViewManager;

import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.beans.PropertyVetoException;
import java.util.ArrayList;

import javax.swing.JInternalFrame;

import nl.Viewer.Application;
import nl.Viewer.GUI;
import nl.Viewer.Dialogs.DatasetSelection.DatasetSelectionTransferObject;
import nl.Viewer.SerializedObjects.Dataset;
import nl.Viewer.SerializedObjects.Modality;
import nl.Viewer.SerializedObjects.Output;
import nl.Viewer.SerializedObjects.Visualization;
import nl.Viewer.Sessions.ClientSideSession;
import nl.Viewer.Sessions.Communication.ObjectTuple;
import nl.Viewer.ViewManager.Controllers.GenericController;
import nl.Viewer.ViewManager.Controllers.ImageAsPlaneController;
import nl.Viewer.ViewManager.Controllers.ImageAsViewController;
import nl.Viewer.ViewManager.Controllers.ZoomifyImageAsPlaneController;
import nl.Viewer.ViewManager.Controllers.ZoomifyImageAsViewController;
import nl.Viewer.ViewManager.Controllers.ZoomifyImageAsImageController;
import nl.Viewer.ViewManager.Objects.ControllerObject;
import nl.Viewer.ViewManager.Objects.ControllerPair;
import nl.Viewer.ViewManager.Objects.OutputVisualizationTuple;
import nl.Viewer.ViewManager.Objects.SynchronizableControllersCollection;

/**
 * This class is responsible for creating the correct controller/view for a selected modality/visualization. The class
 * contains some methods accessible to the viewFrame, for acting upon frame events. Also, the viewManager acts as the
 * drag-and-drop handler.
 * 
 * @author Jeroen Verschuur
 * 
 */

public class ViewManager implements DropTargetListener {
   private GUI                                 gui                                     = null;
   private ViewDesktop                         viewDesktop;
   private static ViewManager                  instance;
   private ArrayList<OutputVisualizationTuple> registeredOutputTypes                   = new ArrayList<OutputVisualizationTuple>();
   public SynchronizableControllersCollection  synchronizableControllers               = new SynchronizableControllersCollection();
   private boolean                             oneInstancePerModalityVisualizationPair = false;

   private ViewManager() {
      registerOutputType( "View", "Zoomify", ZoomifyImageAsViewController.class );
      registerOutputType( "View", "Image", ImageAsViewController.class );

      registerOutputType( "Plane", "Zoomify", ZoomifyImageAsPlaneController.class );
      registerOutputType( "Plane", "Image", ImageAsPlaneController.class );

      registerOutputType( "Image", "Image", ImageAsPlaneController.class );
      registerOutputType( "Image", "Zoomify", ZoomifyImageAsImageController.class );
      

      // create the view panel
      viewDesktop = new ViewDesktop();

      // register the viewManager as the drop target for the viewdesktop component area
      new DropTarget( viewDesktop, DnDConstants.ACTION_LINK, this, true, null );

      viewDesktop.validate();
   }

   /**
    * registerOutputType registers a viewer class with a given output/visualization type. NOTE: The 'output' parameter
    * of the OutputVisualizationTuple is used here for the outputType ('zoomify', 'image', etc)! When a
    * VisualizationView is instantiated the 'output' parameter is replaced by the output name (server-specific:
    * 'ViewToPNG' for example)
    * 
    * @param outputType
    * @param viewClass
    */
   public void registerOutputType( String visualizationType, String outputType,
         Class<? extends GenericController> viewClass ) {
      if( viewClass != null && outputType != null ) {
         registeredOutputTypes.add( new OutputVisualizationTuple( visualizationType.toUpperCase(),
               outputType.toUpperCase(), viewClass ) );
      }
   }

   /**
    * This method finds the most appropriate viewer class for the specified visualization. It also translates from
    * outputDataType to outputName
    * 
    * @param visualization
    * @param modality
    * @return
    * @throws Exception
    */

   public OutputVisualizationTuple getAppropriateViewClass( Visualization visualization, Modality modality ) {
      if( visualization == null || modality == null )
         return null;

      // go through all registered output types, and try to match both the type of visualization (viewport render, plane
      // render, etc) and a possible output (PNG, Zoomify, etc)
      for( OutputVisualizationTuple ovt : registeredOutputTypes ) {
         if( visualization.getVisualizationType().equalsIgnoreCase( ovt.visualizationType ) ) {
            for( Output o : visualization.getOutputs().values() ) {
               if( o.outputDataType.equalsIgnoreCase( ovt.output ) ) {
                  return new OutputVisualizationTuple( ovt.visualizationType, o.outputName, ovt.controllerClass );
               }
            }
         }
      }
      return null;
   }

   /**
    * Get the instance of this singleton class. This method can be accessed by any other class in any other package.
    * 
    * @return this instance
    * @throws Exception
    */
   public synchronized static ViewManager getInstance() {
      if( instance == null ) {
         synchronized( Application.class ) {
            instance = new ViewManager();
         }
      }
      return instance;
   }

   public ViewDesktop getViewDesktop( GUI gui ) {
      this.gui = gui;
      return viewDesktop;
   }

   public void clearViewPanel() {
      viewDesktop.removeAll();
      viewDesktop.validate();
   }

   public void closeSessionForView( GenericController viewController ) {
      if( viewController == null )
         return;

      // remove all references to the controller of the closing view
      synchronizableControllers.removeController( viewController );

      Application.sessionManager.closeSession( viewController.session );
   }

   /**
    * This method will return a GenericController object responsible for the view with the specified visualization and
    * modality. NB. There may be multiple such views, this method will only return the first one.
    * 
    * @param visualization
    * @param m
    * @return null if no view with the given visualization/modality is found
    */
   public GenericController getViewController( Visualization visualization, Modality m ) {
      JInternalFrame[] frames = viewDesktop.getAllFrames();
      for( int i = 0; i < frames.length; i++ ) {
         ViewFrame v = (ViewFrame)frames[i];
         if( v.visualizationController != null && v.visualizationController.visualization == visualization && v.visualizationController.modality == m )
            return v.visualizationController;
      }
      return null;
   }

   public void setOrientationAndParameterTabs( GenericController visualizationController ) {
      // remove all gui elements for this controller
      if( visualizationController == null ) {
         gui.setOrientationTab( null );
         gui.setParameterTab( null );
         gui.setSynchronizationMenu( null, null );
         gui.setActionMenu( null );
         return;
      }

      gui.setOrientationTab( visualizationController.getOrientationPanel() );
      gui.setParameterTab( visualizationController.getParameterPanel() );
      gui.setSynchronizationMenu( visualizationController,
            synchronizableControllers.findPairsForController( visualizationController ) );

      gui.setActionMenu( visualizationController );
   }

   public GenericController getViewController( long sessionId ) {
      JInternalFrame[] frames = viewDesktop.getAllFrames();
      for( int i = 0; i < frames.length; i++ ) {
         ViewFrame v = (ViewFrame)frames[i];
         if( v.visualizationController != null ) {
            if( v.visualizationController.session.sessionId == sessionId )
               return v.visualizationController;
         }
      }
      return null;
   }

   public ArrayList<ControllerPair> getSynchronizableControllers( GenericController activeController ) {
      synchronized( synchronizableControllers ) {
         return synchronizableControllers.findPairsForController( activeController );
      }
   }

   public boolean toggleSynchronization( GenericController activeController, GenericController target ) {
      boolean currentState;
      synchronized( synchronizableControllers ) {
         ControllerPair cp = synchronizableControllers.findPair( activeController, target );
         if( cp == null ) // pair already removed?
            return true;

         // toggle the synchronization state
         ControllerObject activeControllerObject = cp.getObject1();
         ControllerObject targetControllerObject = cp.getObject2();
         if( activeControllerObject.getController() != activeController ) {
            targetControllerObject = activeControllerObject;
            activeControllerObject = cp.getObject2();
         }

         // don't allow the toggle if the active controller is already synchronized to the target controller
         if( targetControllerObject.isOtherControllerSynchronized() )
            return false;

         currentState = activeControllerObject.isOtherControllerSynchronized();
         activeControllerObject.setOtherControllerSynchronized( !currentState );
      }

      // if we enabled the synchronization, synchronize right away
      if( !currentState )
         target.synchronizeWith( activeController );

      return true;
   }

   private void updateSynchronizableControllers( GenericController newController ) {
      JInternalFrame[] frames = viewDesktop.getAllFrames();
      synchronized( synchronizableControllers ) {
         for( int i = 0; i < frames.length; i++ ) {
            ViewFrame v = (ViewFrame)frames[i];
            if( v.visualizationController != null && v.visualizationController != newController ) {
               // check whether the two controllers can be synchronized
               if( v.visualizationController.isSynchronizableWith( newController ) ) {
                  // use the object hashcodes to create a unique key in the hashmap
                  synchronizableControllers.addPair( newController, v.visualizationController );
               }
            }
         }
      }
   }

   public boolean isSynchronizableWith( GenericController source, GenericController target ) {
      return( synchronizableControllers.findPair( source, target ) != null );
   }

   public ViewFrame getControllerFrame( GenericController controller ) {
      JInternalFrame[] frames = viewDesktop.getAllFrames();
      for( int i = 0; i < frames.length; i++ ) {
         ViewFrame v = (ViewFrame)frames[i];
         if( v.visualizationController == controller ) {
            return v;
         }
      }
      return null;
   }

   public void setFrameTitle( GenericController controller, String title ) {
      ViewFrame frame = getControllerFrame( controller );
      if( frame != null )
         frame.setTitle( title );

   }

   public boolean openLookmark( int lookmarkId, int visualizationId, int modalityId ) {
      System.out.println( "Request for opening lookmark " + lookmarkId );

      // start a new session for this lookmark
      ClientSideSession session = null;
      // request a new session for this intanceId
      try {
         session = Application.sessionManager.createSessionFromLookmark( lookmarkId );
      }
      catch( Exception e ) {
         Application.showErrorMessage( "Session error", "Session could not be created: " + e.getMessage() );
         e.printStackTrace();

         return false;
      }

      Dataset selectedDataset = Application.gui.getSelectedDataset();
      Modality modality = selectedDataset.getModality( modalityId );
      Visualization visualization = modality.getVisualization( visualizationId );

      OutputVisualizationTuple tuple = getAppropriateViewClass( visualization, modality );

      if( tuple == null ) {
         Application.showErrorMessage(
               "Visualization error",
               "No compatible view for output " + visualization.getOutputs() + " with visualization type " + visualization.getVisualizationType() );

         Application.sessionManager.closeSession( session );

         return false;
      }

      // create a frame for the view
      ViewFrame frame = null;
      try {
         // the constructor of the ViewFrame will construct a suitable controller, based on the decision above
         // on the outputVisualizationTuple
         frame = new ViewFrame( visualization, modality, session, tuple, viewDesktop );
      }
      catch( Exception e ) {
         Application.showErrorMessage( "View frame error", "Could not create frame: " + e.getMessage() );
         e.printStackTrace();
         return false;
      }
      try {
         // get the parameters from the servers and set them for the VisualizationView
         // as the modality, visualization and the output have been chosen at this point, the
         // set of user-modifiable parameters will not change anymore until the session is closed
         session.getSessionParametersFromServer( session, tuple.output, frame.visualizationController );

         // the server will prepare all parameters with appropriate values from the lookmark parameter values, make sure
         // that we send all parameters along with the next job request
         frame.visualizationController.getDataModel().setAllParametersModified();

         // check which other views may be synchronized to this view
         // 1) there must be synchronizable parameters
         // 2) all parameters requiring synchronization must be the same
         updateSynchronizableControllers( frame.visualizationController );

         frame.visualizationController.initializeParameterComponents();
         viewDesktop.addFrame( frame );
         viewDesktop.tileFrames();

         // start the render loop
         frame.visualizationController.start();
      }
      catch( Exception e ) {
         // show the message
         Application.showErrorMessage( "Initialization error",
               "Could not initialize session or frame layout: " + e.getMessage() );

         e.printStackTrace();
         viewDesktop.remove( frame );

         return false;
      }

      return true;
   }

   public boolean openVisualization( Modality modality, Visualization visualization ) {
      System.out.println( "Request for opening view for " + visualization + " of modality " + modality );

      // check if duplicate modality/visualization pairs when only one instance of such a pair is allowed
      if( oneInstancePerModalityVisualizationPair ) {
         // check if we already opened the view
         GenericController controller = getViewController( visualization, modality );
         if( controller != null ) {
            try {
               getControllerFrame( controller ).setSelected( true );
            }
            catch( PropertyVetoException pve ) {
               // ignore
            }

            return false;
         }
      }
      ClientSideSession session = null;
      // request a new session for this intanceId
      try {
         session = Application.sessionManager.createSession( modality.getModalityId(), visualization.visualizationId );
      }
      catch( Exception e ) {
         Application.showErrorMessage( "Session error", "Session could not be created: " + e.getMessage() );
         e.printStackTrace();

         return false;
      }

      OutputVisualizationTuple tuple = getAppropriateViewClass( visualization, modality );

      if( tuple == null ) {
         Application.showErrorMessage(
               "Visualization error",
               "No compatible view for output " + visualization.getOutputs() + " with visualization type " + visualization.getVisualizationType() );

         Application.sessionManager.closeSession( session );

         return false;
      }

      // create a frame for the view
      ViewFrame frame = null;
      try {
         // the constructor of the ViewFrame will construct a suitable controller, based on the decision above
         // on the outputVisualizationTuple
         frame = new ViewFrame( visualization, modality, session, tuple, viewDesktop );
      }
      catch( Exception e ) {
         Application.showErrorMessage( "View frame error", "Could not create frame: " + e.getMessage() );
         e.printStackTrace();
         return false;
      }
      try {
         // get the parameters from the servers and set them for the VisualizationView
         // as the modality, visualization and the output have been chosen at this point, the
         // set of user-modifiable parameters will not change anymore until the session is closed
         session.getSessionParametersFromServer( session, tuple.output, frame.visualizationController );

         // check which other views may be synchronized to this view
         // 1) there must be synchronizable parameters
         // 2) all parameters requiring synchronization must be the same
         updateSynchronizableControllers( frame.visualizationController );

         frame.visualizationController.initializeParameterComponents();
         viewDesktop.addFrame( frame );
         viewDesktop.tileFrames();

         // start the render loop
         frame.visualizationController.start();
      }
      catch( Exception e ) {
         // show the message
         Application.showErrorMessage( "Initialization error",
               "Could not initialize session or frame layout: " + e.getMessage() );

         e.printStackTrace();
         viewDesktop.remove( frame );

         return false;
      }

      return true;
   }

   public void dragExit( DropTargetEvent dtde ) {
   }

   public void dragEnter( DropTargetDragEvent dtde ) {
   }

   public void drop( DropTargetDragEvent dtde ) {
   }

   public void dropActionChanged( DropTargetDragEvent dtde ) {
   }

   public void dragOver( DropTargetDragEvent dtde ) {
   }

   public void drop( DropTargetDropEvent dtde ) {
      // ignore the action performed, just check the transferables
      DataFlavor[] flavors = dtde.getCurrentDataFlavors();
      for( int i = 0; i < flavors.length; i++ ) {
         try {
            Object td = dtde.getTransferable().getTransferData( flavors[i] );

            DatasetSelectionTransferObject tObject = (DatasetSelectionTransferObject)td;

            for( ObjectTuple tuple : tObject.visualizationsAndModalities ) {
               openVisualization( (Modality)tuple.obj2, (Visualization)tuple.obj1 );
            }
         }
         catch( Exception e ) {
            System.out.println( "Ignoring following exception (for invalid drop content): " );
            e.printStackTrace();
            // ignore
         }
      }
   }

}
